<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useExpressionStore, type ContourSegment } from '@/stores/expressionStore'
import { useSettingsStore } from '@/stores/settingsStore'
import { evaluateExpression } from '@/wasm'

interface Viewport {
  centerX: number
  centerY: number
  scale: number // world units per pixel
}

interface SampleResult {
  id: string
  segments: ContourSegment[]
  sampleValue: number | null
  durationMs: number
  cacheHit: boolean
  validSampleRatio: number
}

interface GridSamplerConfig {
  xMin: number
  xMax: number
  yMin: number
  yMax: number
  columns: number
  rows: number
}

interface CacheEntry {
  segments: ContourSegment[]
  sampleValue: number | null
  durationMs: number
  computedAt: number
  validSamples: number
  totalSamples: number
}

const expressionStore = useExpressionStore()
const settingsStore = useSettingsStore()
const { expressions, visibleExpressions } = storeToRefs(expressionStore)

const canvasRef = ref<HTMLCanvasElement | null>(null)
const wrapperRef = ref<HTMLDivElement | null>(null)
const isComputing = ref(false)
const lastComputationMs = ref(0)
const computationError = ref<string | null>(null)

const viewport = reactive<Viewport>({
  centerX: 0,
  centerY: 0,
  scale: 0.02,
})

const segmentsById = shallowRef(new Map<string, ContourSegment[]>())

const cacheStats = reactive({
  hits: 0,
  misses: 0,
})

const skippedSampleRatio = ref(0)

const pointerState = reactive({
  dragging: false,
  pointerId: null as number | null,
  lastX: 0,
  lastY: 0,
})

const dpr = window.devicePixelRatio ?? 1
const contourCache = new Map<string, CacheEntry>()

const zoomRange = {
  min: 0.002,
  max: 0.2,
}

const axesColor = 'rgba(148, 163, 184, 0.25)'
const gridColor = 'rgba(100, 116, 139, 0.15)'
const backgroundColor = '#0b1120'

const shouldRender = computed(() => segmentsById.value.size > 0)

const expressionPayload = computed(() =>
  visibleExpressions.value.map(({ id, expression, color }) => ({
    id,
    expression,
    color,
  })),
)

let resizeObserver: ResizeObserver | null = null
let recomputeHandle: number | null = null
let renderHandle: number | null = null

function requestRecompute() {
  if (recomputeHandle !== null) {
    cancelAnimationFrame(recomputeHandle)
  }
  recomputeHandle = requestAnimationFrame(() => {
    recomputeHandle = null
    computeContours()
  })
}

function requestRender() {
  if (renderHandle !== null) {
    cancelAnimationFrame(renderHandle)
  }
  drawCanvas()
  renderHandle = requestAnimationFrame(() => {
    renderHandle = null
    drawCanvas()
  })
}

function setCanvasDimensions() {
  const canvas = canvasRef.value
  const wrapper = wrapperRef.value
  if (!canvas || !wrapper) {
    return
  }

  const rect = wrapper.getBoundingClientRect()
  let width = rect.width
  let height = rect.height

  if (!Number.isFinite(width) || width <= 0) {
    width = 960
  }

  if (!Number.isFinite(height) || height <= 0) {
    height = 540
  }

  const targetWidth = Math.max(1, Math.round(width * dpr))
  const targetHeight = Math.max(1, Math.round(height * dpr))
  const changed = canvas.width !== targetWidth || canvas.height !== targetHeight

  canvas.width = targetWidth
  canvas.height = targetHeight

  if (changed) {
    requestRender()
    requestRecompute()
  }
}

function attachResizeObserver() {
  if (!wrapperRef.value) return
  resizeObserver = new ResizeObserver(() => setCanvasDimensions())
  resizeObserver.observe(wrapperRef.value)
}

function detachResizeObserver() {
  if (resizeObserver && wrapperRef.value) {
    resizeObserver.disconnect()
  }
  resizeObserver = null
}

function onPointerDown(event: PointerEvent) {
  if (!canvasRef.value) return
  pointerState.dragging = true
  pointerState.pointerId = event.pointerId
  pointerState.lastX = event.clientX
  pointerState.lastY = event.clientY
  canvasRef.value.setPointerCapture(event.pointerId)
}

function onPointerMove(event: PointerEvent) {
  if (!pointerState.dragging) return
  const dxPixels = event.clientX - pointerState.lastX
  const dyPixels = event.clientY - pointerState.lastY
  pointerState.lastX = event.clientX
  pointerState.lastY = event.clientY

  viewport.centerX -= dxPixels * viewport.scale
  viewport.centerY += dyPixels * viewport.scale

  requestRender()
  requestRecompute()
}

function onPointerUp(event: PointerEvent) {
  if (!pointerState.dragging) return
  if (pointerState.pointerId !== event.pointerId) return
  pointerState.dragging = false
  pointerState.pointerId = null
  const canvas = canvasRef.value
  if (canvas) {
    canvas.releasePointerCapture(event.pointerId)
  }
}

function onWheel(event: WheelEvent) {
  const canvas = canvasRef.value
  if (!canvas) return
  event.preventDefault()

  const rect = canvas.getBoundingClientRect()
  const cursorX = event.clientX - rect.left
  const cursorY = event.clientY - rect.top
  const worldBefore = canvasToWorld(cursorX, cursorY)

  const zoomFactor = event.deltaY > 0 ? 1.1 : 0.9
  const nextScale = clamp(viewport.scale * zoomFactor, zoomRange.min, zoomRange.max)
  viewport.scale = nextScale

  const worldAfter = canvasToWorld(cursorX, cursorY)
  viewport.centerX += worldBefore.x - worldAfter.x
  viewport.centerY += worldBefore.y - worldAfter.y

  requestRender()
  requestRecompute()
}

function canvasToWorld(pixelX: number, pixelY: number) {
  const canvas = canvasRef.value
  if (!canvas) {
    return { x: 0, y: 0 }
  }
  const width = canvas.width / dpr
  const height = canvas.height / dpr
  const x = viewport.centerX + (pixelX - width / 2) * viewport.scale
  const y = viewport.centerY - (pixelY - height / 2) * viewport.scale
  return { x, y }
}

function worldToCanvas(x: number, y: number) {
  const canvas = canvasRef.value
  if (!canvas) {
    return { x: 0, y: 0 }
  }
  const width = canvas.width / dpr
  const height = canvas.height / dpr
  const pixelX = (x - viewport.centerX) / viewport.scale + width / 2
  const pixelY = height / 2 - (y - viewport.centerY) / viewport.scale
  return { x: pixelX, y: pixelY }
}

function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max)
}

function normaliseErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    return error.message
  }
  if (typeof error === 'string') {
    return error
  }
  try {
    return JSON.stringify(error)
  } catch {
    return ''
  }
}

function isDomainError(error: unknown): boolean {
  const message = normaliseErrorMessage(error).toLowerCase()
  if (!message) {
    return false
  }
  return (
    message.includes('expects argument') ||
    message.includes('division by zero') ||
    message.includes('indeterminate') ||
    message.includes('result is nan')
  )
}

async function computeContours() {
  recomputeHandle = null
  const canvas = canvasRef.value
  if (!canvas) {
    return
  }

  const expressionsToRender = expressionPayload.value.filter(
    (item) => item.expression.trim().length > 0,
  )

  if (expressionsToRender.length === 0) {
    segmentsById.value = new Map()
    computationError.value = null
    expressionStore.clearSegments()
    cacheStats.hits = 0
    cacheStats.misses = 0
    skippedSampleRatio.value = 0
    requestRender()
    return
  }

  const width = canvas.width / dpr
  const height = canvas.height / dpr
  const halfWidth = (width / 2) * viewport.scale
  const halfHeight = (height / 2) * viewport.scale

  const bounds = {
    xMin: viewport.centerX - halfWidth,
    xMax: viewport.centerX + halfWidth,
    yMin: viewport.centerY - halfHeight,
    yMax: viewport.centerY + halfHeight,
  }

  const baseGridX = width / 16
  const baseGridY = height / 16
  const zoomFactor = Math.min(6, Math.max(1, Math.sqrt(0.02 / viewport.scale)))
  const deviceFactor = Math.max(1, Math.sqrt(dpr))
  const userFactor = settingsStore.samplingMultiplier
  const resolutionFactor = zoomFactor * deviceFactor * userFactor
  const columns = Math.max(32, Math.round(baseGridX * resolutionFactor))
  const rows = Math.max(32, Math.round(baseGridY * resolutionFactor))

  const samplerConfig: GridSamplerConfig = {
    ...bounds,
    columns,
    rows,
  }

  isComputing.value = true
  computationError.value = null
  expressionStore.markEvaluating(expressionsToRender.map((entry) => entry.id))

  try {
    const startTime = performance.now()
    const results: SampleResult[] = []
    let cacheHits = 0
    let cacheMisses = 0
    let totalValidSamples = 0
    let totalSamples = 0

    for (const item of expressionsToRender) {
      try {
        const cacheKey = createCacheKey(item.expression, samplerConfig)
        const cached = contourCache.get(cacheKey)

        let segments: ContourSegment[] = []
        let sampleValue: number | null = null
        let durationMs = 0
        let cacheHit = false
        let validSamples = 0
        let sampleCount = columns * rows

        if (cached) {
        cacheHits += 1
        segments = cached.segments
        sampleValue = cached.sampleValue
        durationMs = cached.durationMs
        cacheHit = true
        validSamples = cached.validSamples
        sampleCount = cached.totalSamples
      } else {
        cacheMisses += 1
        const computeStart = performance.now()
        const grid = await sampleExpression(item.expression, samplerConfig)
        validSamples = grid.validSamples
        sampleCount = grid.totalSamples
        segments = validSamples > 0 ? runMarchingSquares(grid) : []
        sampleValue = await evaluateSampleValue(item.expression, viewport.centerX, viewport.centerY)
        durationMs = performance.now() - computeStart
        contourCache.set(cacheKey, {
          segments,
          sampleValue,
          durationMs,
          computedAt: Date.now(),
          validSamples,
          totalSamples: sampleCount,
        })
      }
      const ratio = sampleCount === 0 ? 0 : validSamples / sampleCount
        totalValidSamples += validSamples
        totalSamples += sampleCount

        const segmentsClone = cloneSegments(segments)
        results.push({
          id: item.id,
          segments: segmentsClone,
          sampleValue,
          durationMs,
          cacheHit,
          validSampleRatio: ratio,
        })

        expressionStore.setResult(item.id, {
          segments: segmentsClone,
          sampleValue,
          durationMs,
          cacheHit,
          validSampleRatio: ratio,
        })
      } catch (error) {
        const message =
          error instanceof Error
            ? error.message
            : `Expression ${item.expression} failed to evaluate`
        expressionStore.setError(item.id, message)
      }
    }

    const nextSegments = new Map<string, ContourSegment[]>()
    for (const result of results) {
      nextSegments.set(result.id, result.segments)
    }
    segmentsById.value = nextSegments

    cacheStats.hits = cacheHits
    cacheStats.misses = cacheMisses
    lastComputationMs.value = performance.now() - startTime

    skippedSampleRatio.value = totalSamples > 0 ? Math.max(0, 1 - totalValidSamples / totalSamples) : 0

    if (totalSamples > 0 && totalValidSamples === 0) {
      computationError.value = '当前视图超出了表达式的定义域，请尝试平移或缩放。'
    } else {
      computationError.value = null
    }
  } catch (error) {
    computationError.value =
      error instanceof Error ? error.message : 'Contour calculation failed. Please verify expressions.'
  } finally {
    isComputing.value = false
    requestRender()
  }
}

async function sampleExpression(expression: string, config: GridSamplerConfig) {
  const { columns, rows, xMin, xMax, yMin, yMax } = config
  const dx = (xMax - xMin) / (columns - 1)
  const dy = (yMax - yMin) / (rows - 1)
  const values: number[][] = new Array(rows)
  let validSamples = 0
  const totalSamples = columns * rows

  for (let row = 0; row < rows; row += 1) {
    const y = yMax - row * dy
    const promises: Promise<number>[] = []
    for (let col = 0; col < columns; col += 1) {
      const x = xMin + col * dx
      promises.push(evaluateExpression(expression, x, y))
    }

    const settled = await Promise.allSettled(promises)
    const rowValues: number[] = new Array(columns)
    settled.forEach((result, col) => {
      if (result.status === 'fulfilled') {
        const value = result.value
        if (Number.isFinite(value)) {
          rowValues[col] = value
          validSamples += 1
        } else {
          rowValues[col] = Number.NaN
        }
      } else if (isDomainError(result.reason)) {
        rowValues[col] = Number.NaN
      } else {
        throw result.reason ?? new Error('Expression evaluation failed')
      }
    })
    values[row] = rowValues
  }

  return {
    values,
    columns,
    rows,
    xMin,
    yMin,
    dx,
    dy,
    validSamples,
    totalSamples,
  }
}

async function evaluateSampleValue(expression: string, x: number, y: number): Promise<number | null> {
  try {
    const value = await evaluateExpression(expression, x, y)
    return Number.isFinite(value) ? value : null
  } catch (error) {
    if (isDomainError(error)) {
      return null
    }
    throw error
  }
}

function runMarchingSquares(grid: {
  values: number[][]
  columns: number
  rows: number
  xMin: number
  yMin: number
  dx: number
  dy: number
}): ContourSegment[] {
  const segments: ContourSegment[] = []
  const { values, columns, rows, xMin, yMin, dx, dy } = grid
  const yMax = yMin + (rows - 1) * dy
  const edgeTable: Record<number, [number, number][]> = {
    0: [],
    1: [[3, 0]],
    2: [[0, 1]],
    3: [[3, 1]],
    4: [[1, 2]],
    5: [
      [3, 0],
      [1, 2],
    ],
    6: [[0, 2]],
    7: [[3, 2]],
    8: [[2, 3]],
    9: [[0, 2]],
    10: [
      [1, 2],
      [0, 3],
    ],
    11: [[1, 3]],
    12: [[1, 3]],
    13: [[0, 1]],
    14: [[3, 0]],
    15: [],
  }

  for (let row = 0; row < rows - 1; row += 1) {
    const currentRow = values[row]
    const nextRow = values[row + 1]
    if (!currentRow || !nextRow) {
      continue
    }
    for (let col = 0; col < columns - 1; col += 1) {
      const v0 = currentRow[col]
      const v1 = currentRow[col + 1]
      const v2 = nextRow[col + 1]
      const v3 = nextRow[col]
      if (
        v0 === undefined ||
        v1 === undefined ||
        v2 === undefined ||
        v3 === undefined
      ) {
        continue
      }

      if (
        !Number.isFinite(v0) ||
        !Number.isFinite(v1) ||
        !Number.isFinite(v2) ||
        !Number.isFinite(v3)
      ) {
        continue
      }

      const caseIndex =
        (v0 >= 0 ? 1 : 0) |
        (v1 >= 0 ? 2 : 0) |
        (v2 >= 0 ? 4 : 0) |
        (v3 >= 0 ? 8 : 0)

      const edges = edgeTable[caseIndex]
      if (!edges || edges.length === 0) {
        continue
      }

      const topLeft = {
        x: xMin + col * dx,
        y: yMax - row * dy,
      }
      const topRight = {
        x: topLeft.x + dx,
        y: topLeft.y,
      }
      const bottomRight = {
        x: topRight.x,
        y: topRight.y - dy,
      }
      const bottomLeft = {
        x: topLeft.x,
        y: topLeft.y - dy,
      }

      const edgePoints = [
        interpolateZero(topLeft, topRight, v0, v1),
        interpolateZero(topRight, bottomRight, v1, v2),
        interpolateZero(bottomRight, bottomLeft, v2, v3),
        interpolateZero(bottomLeft, topLeft, v3, v0),
      ]

      for (const [edgeA, edgeB] of edges) {
        const start = edgePoints[edgeA]
        const end = edgePoints[edgeB]
        if (start && end) {
          segments.push({ start, end })
        }
      }
    }
  }

  return segments
}

function interpolateZero(
  start: { x: number; y: number },
  end: { x: number; y: number },
  valueStart: number,
  valueEnd: number,
) {
  const EPSILON = 1e-9
  if (Math.abs(valueStart) < EPSILON && Math.abs(valueEnd) < EPSILON) {
    return { ...start }
  }
  if (Math.abs(valueStart - valueEnd) < EPSILON) {
    return { ...start }
  }
  const t = valueStart / (valueStart - valueEnd)
  return {
    x: start.x + t * (end.x - start.x),
    y: start.y + t * (end.y - start.y),
  }
}

function drawCanvas() {
  const canvas = canvasRef.value
  if (!canvas) return
  const context = canvas.getContext('2d')
  if (!context) return
  const width = canvas.width / dpr
  const height = canvas.height / dpr
  context.save()
  context.scale(dpr, dpr)
  context.clearRect(0, 0, width, height)
  context.fillStyle = backgroundColor
  context.fillRect(0, 0, width, height)

  drawGrid(context)
  drawAxes(context)
  drawAxisTicks(context)
  drawContours(context)
  drawOverlay(context)

  context.restore()
}

function cloneSegments(segments: ContourSegment[]): ContourSegment[] {
  return segments.map((segment) => ({
    start: { x: segment.start.x, y: segment.start.y },
    end: { x: segment.end.x, y: segment.end.y },
  }))
}

function createCacheKey(expression: string, config: GridSamplerConfig) {
  const precision = 3
  return [
    expression,
    config.columns,
    config.rows,
    config.xMin.toFixed(precision),
    config.xMax.toFixed(precision),
    config.yMin.toFixed(precision),
    config.yMax.toFixed(precision),
  ].join('|')
}

function drawGrid(context: CanvasRenderingContext2D) {
  const canvas = canvasRef.value
  if (!canvas) return
  const width = canvas.width / dpr
  const height = canvas.height / dpr

  const approximateStep = viewport.scale * 80
  const stepMagnitude = Math.pow(10, Math.floor(Math.log10(approximateStep || 1)))
  const step = Math.max(stepMagnitude, viewport.scale)

  context.strokeStyle = gridColor
  context.lineWidth = Math.max(0.75, 1 / dpr)
  context.beginPath()

  const startX = Math.floor((viewport.centerX - width * viewport.scale * 0.5) / step) * step
  const endX = viewport.centerX + width * viewport.scale * 0.5
  for (let x = startX; x <= endX; x += step) {
    const { x: px } = worldToCanvas(x, 0)
    context.moveTo(px, 0)
    context.lineTo(px, height)
  }

  const startY = Math.floor((viewport.centerY - height * viewport.scale * 0.5) / step) * step
  const endY = viewport.centerY + height * viewport.scale * 0.5
  for (let y = startY; y <= endY; y += step) {
    const { y: py } = worldToCanvas(0, y)
    context.moveTo(0, py)
    context.lineTo(width, py)
  }

  context.stroke()
}

function drawAxes(context: CanvasRenderingContext2D) {
  const canvas = canvasRef.value
  if (!canvas) return
  const width = canvas.width / dpr
  const height = canvas.height / dpr
  const origin = worldToCanvas(0, 0)

  context.strokeStyle = axesColor
  context.lineWidth = Math.max(1, 1.5 / dpr)
  context.beginPath()
  context.moveTo(origin.x, 0)
  context.lineTo(origin.x, height)
  context.moveTo(0, origin.y)
  context.lineTo(width, origin.y)
  context.stroke()
}

function drawAxisTicks(context: CanvasRenderingContext2D) {
  const canvas = canvasRef.value
  if (!canvas) {
    return
  }

  const width = canvas.width / dpr
  const height = canvas.height / dpr
  const origin = worldToCanvas(0, 0)
  const tickStep = computeTickStep(viewport.scale)
  const tickLength = 6
  const decimals = computeTickPrecision(tickStep)

  context.save()
  context.strokeStyle = 'rgba(148, 163, 184, 0.4)'
  context.fillStyle = 'rgba(148, 163, 184, 0.8)'
  context.font = '12px \"JetBrains Mono\", ui-monospace, Menlo, Monaco, Consolas'
  context.textAlign = 'center'
  context.textBaseline = 'top'

  const halfWidth = (width / 2) * viewport.scale
  const halfHeight = (height / 2) * viewport.scale
  const bounds = {
    xMin: viewport.centerX - halfWidth,
    xMax: viewport.centerX + halfWidth,
    yMin: viewport.centerY - halfHeight,
    yMax: viewport.centerY + halfHeight,
  }

  if (origin.y >= 0 && origin.y <= height) {
    const startX = Math.ceil(bounds.xMin / tickStep) * tickStep
    for (let x = startX; x <= bounds.xMax; x += tickStep) {
      if (Math.abs(x) < 1e-9) {
        continue
      }
      const { x: px } = worldToCanvas(x, 0)
      if (px < 0 || px > width) {
        continue
      }
      context.beginPath()
      context.moveTo(px, origin.y - tickLength / 2)
      context.lineTo(px, origin.y + tickLength / 2)
      context.stroke()
      context.fillText(formatTickValue(x, decimals), px, origin.y + tickLength / 2 + 2)
    }
  }

  if (origin.x >= 0 && origin.x <= width) {
    const startY = Math.ceil(bounds.yMin / tickStep) * tickStep
    context.textAlign = 'left'
    context.textBaseline = 'middle'
    for (let y = startY; y <= bounds.yMax; y += tickStep) {
      if (Math.abs(y) < 1e-9) {
        continue
      }
      const { y: py } = worldToCanvas(0, y)
      if (py < 0 || py > height) {
        continue
      }
      context.beginPath()
      context.moveTo(origin.x - tickLength / 2, py)
      context.lineTo(origin.x + tickLength / 2, py)
      context.stroke()
      context.fillText(formatTickValue(y, decimals), origin.x + tickLength / 2 + 4, py)
    }
  }

  if (origin.x >= 0 && origin.x <= width && origin.y >= 0 && origin.y <= height) {
    context.textAlign = 'left'
    context.textBaseline = 'top'
    context.fillText('O', origin.x + 4, origin.y + 2)
  }

  context.restore()
}

function computeTickStep(scale: number): number {
  const desiredPixels = 80
  const rawStep = desiredPixels * scale
  const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep || 1)))
  const normalised = rawStep / magnitude

  let nice = 1
  if (normalised > 5) {
    nice = 10
  } else if (normalised > 2) {
    nice = 5
  } else if (normalised > 1) {
    nice = 2
  }

  return nice * magnitude
}

function computeTickPrecision(step: number): number {
  if (step === 0) {
    return 0
  }
  const log10 = Math.log10(Math.abs(step))
  return Math.max(0, Math.ceil(-log10))
}

function formatTickValue(value: number, decimals: number): string {
  const clean = Math.abs(value) < 1e-9 ? 0 : value
  return clean.toFixed(Math.min(decimals, 6))
}


function drawContours(context: CanvasRenderingContext2D) {
  const map = segmentsById.value
  if (map.size === 0) {
    return
  }
  for (const expression of expressions.value) {
    if (!expression.visible) continue
    const segments = map.get(expression.id)
    if (!segments || segments.length === 0) continue
    context.strokeStyle = expression.color
    context.lineWidth = Math.max(1, 2 / dpr)
    context.beginPath()
    for (const segment of segments) {
      const start = worldToCanvas(segment.start.x, segment.start.y)
      const end = worldToCanvas(segment.end.x, segment.end.y)
      context.moveTo(start.x, start.y)
      context.lineTo(end.x, end.y)
    }
    context.stroke()
  }
}

function drawOverlay(context: CanvasRenderingContext2D) {
  const canvas = canvasRef.value
  if (!canvas) return
  const width = canvas.width / dpr
  const height = canvas.height / dpr

  context.fillStyle = 'rgba(15, 23, 42, 0.75)'
  context.font = '12px "JetBrains Mono", ui-monospace, Menlo, Monaco, Consolas'
  context.textAlign = 'left'
  context.textBaseline = 'top'

  const totalSamples = cacheStats.hits + cacheStats.misses
  const cacheSummary = totalSamples > 0
    ? `Cache: ${cacheStats.hits}/${totalSamples} hit(s)`
    : 'Cache: 0 hit(s)'

  const lines = [
    `Center: (${viewport.centerX.toFixed(2)}, ${viewport.centerY.toFixed(2)})`,
    `Scale: ${viewport.scale.toExponential(2)} units/pixel`,
    cacheSummary,
  ]

  if (skippedSampleRatio.value > 0.0001) {
    lines.push(`Domain loss: ${(skippedSampleRatio.value * 100).toFixed(0)}% samples skipped`)
  }

  if (isComputing.value) {
    lines.push('Status: computing…')
  } else if (computationError.value) {
    lines.push(`Status: ${computationError.value}`)
  } else if (lastComputationMs.value > 0) {
    lines.push(`Duration: ${lastComputationMs.value.toFixed(1)} ms`)
  }

  const padding = 10
  lines.forEach((text, index) => {
    context.fillText(text, padding, padding + index * 16)
  })

  if (!shouldRender.value && !isComputing.value) {
    context.textAlign = 'center'
    context.textBaseline = 'middle'
    context.fillStyle = 'rgba(148, 163, 184, 0.7)'
    context.font = '16px "Segoe UI", sans-serif'
    context.fillText('Add an expression to render contours', width / 2, height / 2)
  }
}

function resetViewport() {
  viewport.centerX = 0
  viewport.centerY = 0
  viewport.scale = 0.02
  requestRender()
  requestRecompute()
}

watch(
  () => expressionPayload.value.map((item) => `${item.id}|${item.expression}|${item.color}`),
  () => requestRecompute(),
)

watch(
  () => settingsStore.samplingMultiplier,
  () => {
    contourCache.clear()
    requestRecompute()
  },
)

watch(segmentsById, () => requestRender())

watch(
  () => [viewport.centerX, viewport.centerY, viewport.scale],
  () => {
    requestRender()
  },
)

onMounted(() => {
  attachResizeObserver()
  setCanvasDimensions()
  requestRecompute()
})

onBeforeUnmount(() => {
  detachResizeObserver()
  if (recomputeHandle !== null) {
    cancelAnimationFrame(recomputeHandle)
  }
  if (renderHandle !== null) {
    cancelAnimationFrame(renderHandle)
  }
})
</script>

<template>
  <section class="plot-wrapper">
    <header class="plot-header">
      <div>
        <h2 class="plot-title">绘图区域</h2>
        <p class="plot-subtitle">拖动平移，滚轮缩放。双击可还原视图。</p>
      </div>
      <div class="plot-actions">
        <button type="button" class="plot-action" @click="resetViewport">重置视图</button>
      </div>
    </header>

    <div ref="wrapperRef" class="plot-canvas-container">
      <canvas
        ref="canvasRef"
        class="plot-canvas"
        @pointerdown="onPointerDown"
        @pointermove="onPointerMove"
        @pointerup="onPointerUp"
        @pointerleave="onPointerUp"
        @wheel="onWheel"
        @dblclick="resetViewport"
      ></canvas>
      <div v-if="isComputing" class="plot-overlay">
        <span>计算中…</span>
      </div>
    </div>
  </section>
</template>

<style scoped>
.plot-wrapper {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  flex: 1;
  min-height: 0;
}

.plot-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
}

.plot-title {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 600;
}

.plot-subtitle {
  margin: 0.35rem 0 0;
  font-size: 0.85rem;
  color: #94a3b8;
}

.plot-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.plot-action {
  background: rgba(56, 189, 248, 0.2);
  border: 1px solid rgba(56, 189, 248, 0.4);
  color: #38bdf8;
  border-radius: 0.6rem;
  padding: 0.5rem 0.8rem;
  font-size: 0.9rem;
  cursor: pointer;
  transition: background 120ms ease, transform 120ms ease;
}

.plot-action:hover {
  background: rgba(56, 189, 248, 0.35);
  transform: translateY(-1px);
}

 .plot-canvas-container {
  position: relative;
  flex: 1;
  min-height: 540px;
  border-radius: 0.9rem;
  border: 1px solid rgba(148, 163, 184, 0.2);
  overflow: hidden;
  background: rgba(15, 23, 42, 0.85);
}

.plot-canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
}

.plot-canvas:active {
  cursor: grabbing;
}

.plot-overlay {
  position: absolute;
  inset: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0.75rem;
  pointer-events: none;
  font-size: 0.9rem;
  color: #facc15;
}
</style>
